/*
 * Copyright 2011 The Rusted Roof, LLC.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package org.proteusframework.core.base;

import org.proteusframework.core.api.IPlatformDelegate;
import org.proteusframework.core.api.IPluginRegistrar;
import org.proteusframework.core.api.model.*;
import org.proteusframework.core.spi.IRegistrar;
import org.proteusframework.core.spi.ProteusPluginFactory;
import org.proteusframework.core.util.Assert;

import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Base class for all {@link IPluginRegistrar} instances.
 *
 * @author Tacoma Four
 */
public abstract class AbstractPluginRegistrar<T extends ProteusPluginFactory> extends DisplayableName implements IPluginRegistrar
{
    /**
     * Logger.
     */
    private static final Logger logger = Logger.getLogger(AbstractPluginRegistrar.class.getCanonicalName());

    /**
     * Maps the <code>INamespace</code> to its associated <code>IRegistrar</code>.
     */
    private final Map<Integer, IRegistrar> registrar = new HashMap<Integer, IRegistrar>();

    /**
     * Maps the <code>INamespace</code> to its associated version collection. In other words, one
     * plugin may optionally support more than one plugin API version concurrently.
     */
    private final Map<INamespace, IVersionCollection> versions = new HashMap<INamespace, IVersionCollection>();

    /**
     * Immutable version collection defined by the plugin archetype's PluginRegistrar.
     */
    private final IVersionCollection versionCollection;

    /**
     * Physical class of the plugin factory.
     */
    private final Class<T> pluginFactoryClass;

    /**
     * Injected platform delegate used to access platform services, including locating and instantiating other
     * Proteus objects.
     */
    protected IPlatformDelegate delegate = null;

    /**
     * Class that represents the extended capabilities that are available via the delegate after a cast.
     */
    protected Class<? extends IPlatformDelegate> delegateInterface;

    /**
     * Collection of additional properties describing this object.
     */
    private Properties metadataProperties = new Properties();

    /**
     * Constructs an abstract plugin registrar object associated with the immutable <code>IGlyph</code>
     * that supports the declared version collection.
     *
     * @param displayableName   Displayable name
     * @param versionCollection Collection of versions that this plugin registrar is capable of producing
     */
    protected AbstractPluginRegistrar(final IDisplayableName displayableName,
                                      final IVersionCollection versionCollection,
                                      final Class<T> pluginFactoryClass)
    {
        super(displayableName);

        Assert.parameterNotNull(versionCollection, "Parameter 'versionCollection' must not be null");
        Assert.parameterNotNull(pluginFactoryClass, "Parameter 'pluginFactoryClass' must not be null");

        this.versions.put(displayableName, versionCollection);

        this.versionCollection = versionCollection;
        this.pluginFactoryClass = pluginFactoryClass;

        onDiscovery();
    }

    private void onDiscovery()
    {
        ServiceLoader<T> loader = ServiceLoader.load(pluginFactoryClass);
        for (T pluginFactory : loader)
        {
            String displayName = pluginFactory.getDisplayableName().toString();
            logger.info("Registering plugin factory " + displayName + " with " + pluginFactoryClass.getName() + ".");

            getRegistrar().put(pluginFactory.getDisplayableName().hashCode(), pluginFactory);

            versionCollection.registerVersion(pluginFactory.getDisplayableName(), pluginFactory.supportedVersions());
        }
    }

    /**
     * Plugin registrar map.
     *
     * @return Map of plugin registrars by namespace
     */
    protected Map<Integer, IRegistrar> getRegistrar()
    {
        return registrar;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final <T extends IPlatformDelegate> void setPlatformDelegate(IPlatformDelegate delegate,
                                                                        Class<T> delegateInterface)
    {
        Assert.unitialized(this.delegate, "Application platform delegate is implicitly final and cannot be modified.");

        Assert.parameterNotNull(delegate, "Parameter 'delegate' must not be null");

        this.delegate = delegate;
        this.delegateInterface = delegateInterface;
        onDelegateInjected();
    }

    /**
     * Empty delegate injection event handler for descendant classes.
     */
    protected void onDelegateInjected()
    {

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final IVersionCollection getVersionCollection(INamespace namespace) throws NoSuchNamespaceException
    {
        if (!isInstalled(namespace))
        {
            throw new NoSuchNamespaceException(namespace);
        }

        return versions.get(namespace);
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public final boolean isInstalled(IVersion version)
    {
        boolean found = false;

        Assert.parameterNotNull(version, "Parameter 'version' must not be null");

        for (IRegistrar factory : registrar.values())
        {
            if (factory.supportsVersion(version))
            {
                found = true;
                break;

            }
        }

        return found;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final boolean isInstalled(INamespace namespace)
    {
        Assert.parameterNotNull(namespace, "Parameter 'namespace' must not be null");
        IRegistrar factory = registrar.get(namespace.hashCode());
        return (null != factory);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final boolean isInstalled(IVersionDescriptor versionDescriptor)
    {
        boolean isInstalled = false;

        Assert.parameterNotNull(versionDescriptor, "Parameter 'versionDescriptor' must not be null");

        if (isInstalled(versionDescriptor.getNamespace()))
        {
            IRegistrar factory = registrar.get(versionDescriptor.getNamespace().hashCode());
            if (null != factory)
            {
                isInstalled = factory.supportsVersion(versionDescriptor.getVersion());
            }
        }

        return isInstalled;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Collection<INamespace> qualifiedNames()
    {
        Collection<INamespace> list = new ArrayList<INamespace>();

        for (IRegistrar registered : registrar.values())
        {
            list.add(registered.getDisplayableName());
        }

        return list;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Set<IVersion> versions()
    {
        Set<IVersion> versionListing = new TreeSet<IVersion>();

        for (IVersionCollection curVersionCollection : versions.values())
        {
            versionListing.addAll(curVersionCollection.supportedVersions());
        }

        return versionListing;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Collection<IVersion> list(INamespace namespace) throws NoSuchNamespaceException
    {
        Collection<IVersion> versionList = new ArrayList<IVersion>();

        Assert.parameterNotNull(namespace, "Parameter 'namespace' must not be null");

        if (isInstalled(namespace))
        {
            IRegistrar pluginRegistrar = registrar.get(namespace);
            versionList.addAll(pluginRegistrar.supportedVersions());
        } else
        {
            throw new NoSuchNamespaceException(namespace);
        }

        return versionList;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final <T extends IPlugin> T createPlugin(final IVersion version, final Class<T> expectedInterface)
            throws NoSuchVersionException
    {
        Assert.parameterNotNull(version, "Parameter 'version' must not be null");

        T plugin = null;

        if (isInstalled(version))
        {
            if (logger.isLoggable(Level.FINE))
            {
                logger.fine("Creating default plugin for version " + version.toString());
            }

            for (final IRegistrar factory : registrar.values())
            {
                if (factory.supportsVersion(version))
                {
                    IVersionDescriptor versionDescriptor = new VersionDescriptor(factory.getDisplayableName(), version);

                    plugin = factory.create(versionDescriptor, expectedInterface);
                    plugin.setPlatformDelegate(delegate, delegateInterface);

                    break;
                }
            }

        } else
        {
            throw new NoSuchVersionException(version, "The requested version was not installed");
        }

        if (null == plugin)
        {
            throw new NoSuchVersionException(version, "Failed to identify the given version in the registrar");
        }

        return plugin;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final IVersionCollection getVersionCollection()
    {
        return this.versionCollection;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final <T extends IPlugin> T createPlugin(IVersionDescriptor versionDescriptor, Class<T> expectedInterface)
            throws NoSuchNamespaceException, NoSuchVersionException
    {
        Assert.parameterNotNull(versionDescriptor,
                "Parameter 'versionDescriptor' must not be null");

        Assert.parameterNotNull(versionDescriptor.getVersion(),
                "Parameter 'versionDescriptor.getVersion()' must not be null");

        if (null == versionDescriptor.getNamespace())
        {
            return createPluginByVersion(versionDescriptor, expectedInterface);
        } else
        {
            return createPluginByVersionAndQualifiedName(versionDescriptor, expectedInterface);
        }
    }

    /**
     * Creates a plugin instance based on the version alone, e.g. create the default Foo widget, not a specific
     * implementation of Foo.
     *
     * @param versionDescriptor Version descriptor
     * @param expectedInterface Expected interface type
     * @param <T>               Specifies the type of interface that the plugin is expected to implement
     * @return New plugin instance
     * @throws NoSuchNamespaceException when the namespace specified in the version descriptor doesn't exist
     * @throws NoSuchVersionException   when the version specified in the version descriptor couldn't be found
     *                                  for the given namespace
     */
    private <T extends IPlugin> T createPluginByVersion(IVersionDescriptor versionDescriptor,
                                                        Class<T> expectedInterface)
            throws NoSuchNamespaceException, NoSuchVersionException
    {
        T plugin = null;

        IVersion version = versionDescriptor.getVersion();

        //
        // Only verifies the presence of the version, not the configuration restrictions
        //
        if (isInstalled(version))
        {

            for (final IRegistrar factory : registrar.values())
            {
                if (factory.supportsVersion(version))
                {
                    plugin = factory.create(versionDescriptor, expectedInterface);
                    plugin.setPlatformDelegate(delegate, delegateInterface);

                    break;
                }
            }

            Assert.isNotNull(plugin);

        } else
        {
            if (isInstalled(versionDescriptor.getNamespace()))
            {
                logger.warning("Plugin Registrar has no record of a plugin version " +
                        versionDescriptor.getVersion().toString());

                throw new NoSuchVersionException(versionDescriptor);
            } else
            {
                logger.warning("Plugin Registrar has no record of a plugin " +
                        versionDescriptor.getNamespace().toString());

                throw new NoSuchNamespaceException(versionDescriptor.getNamespace());
            }

        }

        return plugin;
    }

    /**
     * Creates a plugin instance based on the version and namespace, e.g. create <i>this</i> Foo widget of <i>this</i>
     * version.
     *
     * @param versionDescriptor Version descriptor
     * @param expectedInterface Expected interface type
     * @param <T>               Specifies the type of interface that the plugin is expected to implement
     * @return New plugin instance
     * @throws NoSuchNamespaceException when the namespace specified in the version descriptor doesn't exist
     * @throws NoSuchVersionException   when the version specified in the version descriptor couldn't be found
     *                                  for the given namespace
     */
    private <T extends IPlugin> T createPluginByVersionAndQualifiedName(IVersionDescriptor versionDescriptor,
                                                                        Class<T> expectedInterface)
            throws NoSuchNamespaceException, NoSuchVersionException
    {
        T plugin;

        //
        // Only verifies the presence of the version, not the configuration restrictions
        //
        if (isInstalled(versionDescriptor))
        {
            if (logger.isLoggable(Level.FINE))
            {
                logger.fine("Creating " + versionDescriptor.toString());
            }

            IRegistrar pluginRegistrar = registrar.get(versionDescriptor.getNamespace().hashCode());
            plugin = pluginRegistrar.create(versionDescriptor, expectedInterface);
            plugin.setPlatformDelegate(delegate, delegateInterface);

        } else
        {
            if (isInstalled(versionDescriptor.getNamespace()))
            {
                logger.warning("Plugin Registrar has no record of a plugin version " +
                        versionDescriptor.getVersion().toString());

                throw new NoSuchVersionException(versionDescriptor);
            } else
            {
                logger.warning("Plugin Registrar has no record of a plugin " +
                        versionDescriptor.getNamespace().toString());

                throw new NoSuchNamespaceException(versionDescriptor.getNamespace());
            }

        }

        return plugin;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Object getAdapter(Class adapter)
    {
        Assert.parameterNotNull(adapter, "Parameter 'adapter' must not be null");
        return delegate.getAdapterManager().getAdapter(this, adapter);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Properties getMetadata()
    {
        return metadataProperties;
    }
}
